/*
 ***************************************************************************************
 *  Copyright (C) 2006 EsperTech, Inc. All rights reserved.                            *
 *  http://www.espertech.com/esper                                                     *
 *  http://www.espertech.com                                                           *
 *  ---------------------------------------------------------------------------------- *
 *  The software in this package is published under the terms of the GPL license       *
 *  a copy of which has been included with this distribution in the license.txt file.  *
 ***************************************************************************************
 */
package com.espertech.esper.common.internal.schedule;

import com.espertech.esper.common.internal.type.CronParameter;
import com.espertech.esper.common.internal.type.NumberSetParameter;
import com.espertech.esper.common.internal.type.ScheduleUnit;
import com.espertech.esper.common.internal.type.WildcardParameter;

import java.util.EnumMap;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Utility for computing from a set of parameter objects a schedule specification carry a
 * crontab-like schedule definition.
 */
public class ScheduleSpecUtil {
    /**
     * Compute from parameters a crontab schedule.
     *
     * @param args parameters
     * @return crontab schedule
     * @throws ScheduleParameterException if the parameters are invalid
     */
    public static ScheduleSpec computeValues(Object[] args) throws ScheduleParameterException {
        if (args.length <= 4 || args.length >= 10) {
            throw new ScheduleParameterException(getExpressionCountException(args.length));
        }
        EnumMap<ScheduleUnit, SortedSet<Integer>> unitMap = new EnumMap<ScheduleUnit, SortedSet<Integer>>(ScheduleUnit.class);
        Object minutes = args[0];
        Object hours = args[1];
        Object daysOfMonth = args[2];
        Object months = args[3];
        Object daysOfWeek = args[4];
        unitMap.put(ScheduleUnit.MINUTES, computeValues(minutes, ScheduleUnit.MINUTES));
        unitMap.put(ScheduleUnit.HOURS, computeValues(hours, ScheduleUnit.HOURS));
        SortedSet<Integer> resultMonths = computeValues(months, ScheduleUnit.MONTHS);
        if (daysOfWeek instanceof CronParameter && daysOfMonth instanceof CronParameter) {
            throw new ScheduleParameterException("Invalid combination between days of week and days of month fields for timer:at");
        }
        if (resultMonths != null && resultMonths.size() == 1 && (resultMonths.first() instanceof Integer)) {
            // If other arguments are cronParameters, use it for later computations
            CronParameter parameter = null;
            if (daysOfMonth instanceof CronParameter) {
                parameter = (CronParameter) daysOfMonth;
            } else if (daysOfWeek instanceof CronParameter) {
                parameter = (CronParameter) daysOfWeek;
            }
            if (parameter != null) {
                parameter.setMonth(resultMonths.first());
            }
        }
        SortedSet<Integer> resultDaysOfWeek = computeValues(daysOfWeek, ScheduleUnit.DAYS_OF_WEEK);
        SortedSet<Integer> resultDaysOfMonth = computeValues(daysOfMonth, ScheduleUnit.DAYS_OF_MONTH);
        if (resultDaysOfWeek != null && resultDaysOfWeek.size() == 1 && (resultDaysOfWeek.first() instanceof Integer)) {
            // The result is in the form "last xx of the month
            // Days of week is replaced by a wildcard and days of month is updated with
            // the computation of "last xx day of month".
            // In this case "days of month" parameter has to be a wildcard.
            if (resultDaysOfWeek.first() > 6) {
                if (resultDaysOfMonth != null) {
                    throw new ScheduleParameterException("Invalid combination between days of week and days of month fields for timer:at");
                }
                resultDaysOfMonth = resultDaysOfWeek;
                resultDaysOfWeek = null;
            }
        }
        if (resultDaysOfMonth != null && resultDaysOfMonth.size() == 1 && (resultDaysOfMonth.first() instanceof Integer)) {
            if (resultDaysOfWeek != null) {
                throw new ScheduleParameterException("Invalid combination between days of week and days of month fields for timer:at");
            }
        }
        unitMap.put(ScheduleUnit.DAYS_OF_WEEK, resultDaysOfWeek);
        unitMap.put(ScheduleUnit.DAYS_OF_MONTH, resultDaysOfMonth);
        unitMap.put(ScheduleUnit.MONTHS, resultMonths);
        if (args.length > 5) {
            unitMap.put(ScheduleUnit.SECONDS, computeValues(args[5], ScheduleUnit.SECONDS));
        }
        String timezone = null;
        if (args.length > 6) {
            if (!(args[6] instanceof WildcardParameter)) {
                if (!(args[6] instanceof String)) {
                    throw new ScheduleParameterException("Invalid timezone parameter '" + args[6] + "' for timer:at, expected a string-type value");
                }
                timezone = (String) args[6];
            }
        }
        if (args.length > 7) {
            unitMap.put(ScheduleUnit.MILLISECONDS, computeValues(args[7], ScheduleUnit.MILLISECONDS));
        }
        if (args.length > 8) {
            unitMap.put(ScheduleUnit.MICROSECONDS, computeValues(args[8], ScheduleUnit.MICROSECONDS));
        }
        CronParameter optionalDayOfMonthOp = getOptionalSpecialOp(daysOfMonth);
        CronParameter optionalDayOfWeekOp = getOptionalSpecialOp(daysOfWeek);
        return new ScheduleSpec(unitMap, timezone, optionalDayOfMonthOp, optionalDayOfWeekOp);
    }

    public static String getExpressionCountException(int length) {
        return "Invalid number of crontab parameters, expecting between 5 and 7 parameters, received " + length;
    }

    private static CronParameter getOptionalSpecialOp(Object unitParameter) {
        if (!(unitParameter instanceof CronParameter)) {
            return null;
        }
        return (CronParameter) unitParameter;
    }

    private static SortedSet<Integer> computeValues(Object unitParameter, ScheduleUnit unit) throws ScheduleParameterException {
        if (unitParameter instanceof Integer) {
            SortedSet<Integer> result = new TreeSet<Integer>();
            result.add((Integer) unitParameter);
            return result;
        }

        // cron parameters not handled as number sets
        if (unitParameter instanceof CronParameter) {
            return null;
        }

        NumberSetParameter numberSet = (NumberSetParameter) unitParameter;
        if (numberSet.isWildcard(unit.min(), unit.max())) {
            return null;
        }

        Set<Integer> result = numberSet.getValuesInRange(unit.min(), unit.max());
        SortedSet<Integer> resultSorted = new TreeSet<Integer>();
        resultSorted.addAll(result);

        return resultSorted;
    }
}
